package com.neuronbit.businessflow.flow.source;

import com.neuronbit.businessflow.flow.source.exception.ForkAndJoinNotMatchException;
import com.neuronbit.businessflow.flow.source.util.SourceContext;
import com.neuronbit.businessflow.intellij.model.*;
import com.neuronbit.businessflow.runtime.StepType;
import com.neuronbit.javaparser.ast.Modifier;
import com.neuronbit.javaparser.ast.NodeList;
import com.neuronbit.javaparser.ast.body.MethodDeclaration;
import com.neuronbit.javaparser.ast.body.Parameter;
import com.neuronbit.javaparser.ast.expr.ObjectCreationExpr;
import com.neuronbit.javaparser.ast.nodeTypes.NodeWithStatements;
import com.neuronbit.javaparser.ast.stmt.BlockStmt;
import com.neuronbit.javaparser.ast.stmt.CatchClause;
import com.neuronbit.javaparser.ast.stmt.TryStmt;
import com.neuronbit.javaparser.ast.type.ClassOrInterfaceType;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;

import static java.lang.String.format;
import static org.apache.commons.lang3.StringUtils.capitalize;

;

/**
 * The type Fork node source generator.
 */
public class ForkStepJavaSource extends ConditionalStepJavaSource {

    private Join joinNode;

    /**
     * Instantiates a new Fork node source generator.
     *
     * @param node          the node
     * @param sourceContext the source context
     */
    public ForkStepJavaSource(Node node, SourceContext sourceContext) {
        super(node, sourceContext);
    }

    @Override
    protected StepType getStepType() {
        return StepType.FORK;
    }

    @Override
    public void writeStepCallStmt(BlockStmt methodBody) {
        final List<Join> joins = new ArrayList<>();
        findJoin(getNode(), joins);
        if (joins.isEmpty()) {
            throw new ForkAndJoinNotMatchException(String.format("Fork Node %s has no Join", getNode().getName()));
        }
        joinNode = joins.get(0);
        final String joinNodeId = joinNode.getId();
        if (joins.size() > 1) {
            for (Join join : joins) {
                if (!joinNodeId.equals(join.getId())) {
                    throw new ForkAndJoinNotMatchException(String.format("Fork Node %s has more than one Join", getNode().getName()));
                }
            }
        }

        addImport(SimpleType.of(List.class.getCanonicalName()));
        addImport(SimpleType.of(ArrayList.class.getCanonicalName()));
        methodBody.addStatement(String.format("List<Callable<Object>> callables = new ArrayList<>(%s);", getFlow().incomingEdges(joinNode).size()));

        for (Edge outgoingEdge : getFlow().outgoingEdges(getNode())) {
            ObjectCreationExpr anonymousClassExpr = new ObjectCreationExpr();
            anonymousClassExpr.setType(Callable.class);
            addImport(SimpleType.of(Callable.class.getCanonicalName()));

            MethodDeclaration runMethod = new MethodDeclaration();
            runMethod.setName("call");
            runMethod.setModifiers(Modifier.Keyword.PUBLIC);
            runMethod.setType(Object.class);

            BlockStmt stepCallStmt = new BlockStmt();
            handlerSuccessor(outgoingEdge.getTargetNode(), stepCallStmt);
            stepCallStmt.addStatement("return null;");
            BlockStmt catchBody = new BlockStmt();
            catchBody.addStatement("throw new RuntimeException();");
            CatchClause catchClause = new CatchClause(new Parameter(new ClassOrInterfaceType(Exception.class.getSimpleName()), "e"), catchBody);

            BlockStmt runMethodBody = new BlockStmt();
            runMethodBody.addStatement(new TryStmt(stepCallStmt, NodeList.nodeList(catchClause), null));
            runMethod.setBody(runMethodBody);

            anonymousClassExpr.setAnonymousClassBody(NodeList.nodeList(runMethod));

            methodBody.addStatement(format("callables.add(%s);", anonymousClassExpr));
        }

        addImport(SimpleType.of(Future.class.getCanonicalName()));
        Integer timeout = joinNode.getTimeout();
        if (null != timeout) {
            addImport(SimpleType.of("java.util.concurrent.TimeUnit"));
            methodBody.addStatement(format("List<Future<Object>> futures = flow.getExecutor().invokeAll(callables, %s, TimeUnit.MILLISECONDS);", timeout));
        } else {
            methodBody.addStatement("List<Future<Object>> futures = flow.getExecutor().invokeAll(callables);");
        }

        methodBody.addStatement("for (Future<Object> future : futures) { future.get(); }");
    }

    @Override
    protected void afterStepCallStmt(BlockStmt stepCallStmt, BlockStmt methodBody) {
        super.afterStepCallStmt(stepCallStmt, methodBody);

        handlerSuccessor(joinNode, methodBody);
    }

    private void findJoin(Node node, List<Join> joins) {
        if (null == node) {
            return;
        }
        Set<Edge> edges = getFlow().outgoingEdges(node);
        for (Edge edge : edges) {
            Node targetNode = edge.getTargetNode();
            if (targetNode instanceof End) {
                return;
            }
            if (targetNode instanceof Join) {
                joins.add((Join) targetNode);
                break;
            } else {
                findJoin(targetNode, joins);
            }
        }
    }
}
